// The code in this file parses the lines of text stored in the lines variable
// (see "collect text.js") according to the layout of the ALEKS report, 
// "Massachusetts standards report for a single student", with all standards
// expanded to show individual ALEKS items.
// Parsing involves the following calls, in order (see "extract ALEKS data.user.js"):
//    scanSectionHeads(lines)
//    checkSectionHeads()
//    findTitles(lines)
//    organizeLines(lines)
// The result is a nested map of strands, standards, and mastered and unmastered
// ALEKS items in the reportStructure variable.

var dateLineExp = /^\s*(\d+)\/(\d+)\/(\d+)\s*$/;
    // subexpressions: month number; day number; year
var courseLineExp = /^\s*(\d{1,2})(?:st|nd|rd|th)\s+Grade\s+(.*)/i;
    // subexpressions: grade; course title
var studentLineExp = /Massachusetts (?:Mathematics )?Curriculum Framework Learning Standards for Grade\s+([-,+\d]+)\s+Report for\s+(.*)/i;
    // subexpressions: grade(s); student name
var strands = [
    "Number Sense and Operations",
    "Patterns, Relations, and Algebra",
    "Geometry",
    "Measurement",
    "Data Analysis, Statistics, and Probability"
    ];
var strandExps = strands.map( function(s) { return new RegExp('^\\s*' + s, 'i') } );
var strandIds = ['N', 'P', 'G', 'M', 'D'];  // in same order as strands list
var gradeSubjectIds = ['K', '1', '2', '3', '4', '5', '6', '7', '8', 
    '9', '10', '11', '12', 'AI', 'AII', 'PC'];
var standardLineExp = new RegExp('(' + gradeSubjectIds.join('|') + ')' + '\\.' +
        '(' + strandIds.join('|') + ')' + '\\.' + '(\\d+)\\b' + ':\s*' + '(.*)');
    // subexpressions: grade/subject; strand ID; standard number; and standard text
var outOfLineExp = /^\s*(\d+)\s+out\s+of\s+(\d+)\s*$/i;
    // subexpressions: number of items mastered; number of items
var naExp = /^\s*N\/A\b/i;  // sometimes seen instead of "0 out of 0"
var itemsMasteredLineExp = /^\s*(Items|Topics) Mastered:?\s*/i;
var itemsNotMasteredLineExp = /^\s*(Items|Topics) Not Mastered:?\s*/i;
var noneExp = /^\s*none\s*$/i;  // item "none"
var stopExp = /copyright.*ALEKS/i;  // look for this after the last standard line

var reportDate;  // date
var courseLine;  // number
var grade, course;  // strings
var studentName;  // string
var strandLines = [];       // line numbers of strand names
    // eg, {strand: 0 line: 25} might be one entry
var standardLines = [];     // line numbers of standard identifiers
    // eg, {grade: 5, strand: 0, standardNum: 1, standardText: "Add 2 and 2", line: 32, items: 8, mastered: 4}
    // The items and mastered attributes come from the "n out of n" line that follows the standard line.
var outOfLines = [];  // simple line numbers of "n out of n" lines
var masteredLines = [];     // simple line numbers of "Items Mastered" lines
var notMasteredLines = [];  // simple line numbers of "Items Not Mastered" lines
var reportStructure = [];
    // nested map of strand, standard, mastered and unmastered item line numbers
    // See organizeLines().

function numOrder(a, b) { return a - b }  // order function for numeric sorting

function issueWarning(msg) {
    console.log('WARNING: ' + msg);
}

function fatal(msg) {
    throw('FATAL EXCEPTION: ' + msg);
}

function matchStrandName(line) {
    // Return an index into strands if this line matches one, else return -1.
    var strand;  // index
    for (strand = 0; strand < strands.length; strand++) {
        if (strandExps[strand].test(line))
            return strand;
    }
    return -1;  // not found
}

function scanSectionHeads(lines) {
    // Find strand names, standard identifiers, and "Mastered", "Not Mastered" headers.
    var line;  // index into lines
    var strand;  // index into strands
    var standardMatch;  // results of standard identifier match
    var outOfMatch;  // results of "n out of n" match
    var nMastered, nItems;  // parsed from "n out of n" line
    for (line = 0; line < lines.length; line++) {
        // Correct an ALEKS typo.
        if (lines[line].toLowerCase() == 'numbers sense and operations')
            lines[line] = 'Number Sense and Operations';
        // Look for headers of various kinds.
        strand = matchStrandName(lines[line]);
        if (strand >= 0) {
            strandLines.push( { strand: strand, line: line } );
            continue;
        }
        if (itemsMasteredLineExp.test(lines[line])) {
            masteredLines.push(line);
            continue;
        }
        if (itemsNotMasteredLineExp.test(lines[line])) {
            notMasteredLines.push(line);
            continue;
        }
        standardMatch = standardLineExp.exec(lines[line]);
        if (standardMatch) {
            outOfMatch = outOfLineExp.exec(lines[line + 1]);
            if (outOfMatch) {
                nMastered = Number(outOfMatch[1]);
                nItems = Number(outOfMatch[2]);
                outOfLines.push(line + 1);
            } else if (naExp.test(lines[line + 1])) {
                nMastered = nItems = 0;
                outOfLines.push(line + 1);
            } else {
                nMastered = null;
                nItems = null;
                issueWarning('Standard line ' + line + ' not followed by "n out of n" line');
            }
            standardLines.push( {
                grade: standardMatch[1],
                strand: strandIds.indexOf(standardMatch[2]),
                standardNum: Number(standardMatch[3]),
                standardText: standardMatch[4].trim(),
                line: line,
                mastered: nMastered,
                items: nItems
                } );
            if (nMastered !== null) line++;
            continue;
        }
    }
}

function checkSectionHeads() {
    // See if we have something reasonable.
    // Are all strands present and represented by standards?
    var strand;  // index
    for (strand = 0; strand < strands.length; strand++) {
        if ( ! strandLines.some( function(s) { return s.strand == strand } ) )
            issueWarning('Strand not found: "' + strands[strand] + '"');
    }
    // Found some standard(s) and item list headers?
    if (standardLines.length == 0)
        fatal('No framework standards found');
    if (masteredLines.length == 0)
        fatal('No "Items Mastered" header found');
    if (notMasteredLines.length == 0)
        fatal('No "Items Not Mastered" header found');
}

function findTitles(lines) {
    // Find course title, student name, and report date.
    var frontLines = lines.slice(0, strandLines[0].line);
    var courseLineMatch;
    var studentLineMatch;
    var dateLineMatch;
    for (var line = 0; line < frontLines.length; line++) {
        courseLineMatch = courseLineExp.exec(lines[line]);
        if (courseLineMatch) {
            courseLine = line;
            grade = courseLineMatch[1];
            course = courseLineMatch[2];
            continue;
        }
        studentLineMatch = studentLineExp.exec(lines[line]);
        if (studentLineMatch) {
            studentName = studentLineMatch[2];
            continue;
        }
        dateLineMatch = dateLineExp.exec(lines[line]);
        if (dateLineMatch) {
            reportDate = new Date(dateLineMatch[3], dateLineMatch[1], dateLineMatch[2]);
            continue;
        }
    }
}

function organizeLines(lines) {
    // Recorded information will be in line order since that's how we scanned.
    var iStrandLine, iStandardLine, iOutOfLine, iMasteredLine, iNotMasteredLine;  
        // indexes into respective line accumulators - strandLines, standardLines, etc.
    iStrandLine = iStandardLine = iOutOfLine = iMasteredLine = iNotMasteredLine = 0;
    var thisStrand, thisStandard;
    var mastered;  // true in "mastered" list, false in "not mastered" list
    var line = strandLines[0].line;  // line index
    while (line < lines.length) {
        if (iStrandLine < strandLines.length && line == strandLines[iStrandLine].line) {
            reportStructure.push( { strand: strandLines[iStrandLine].strand, standards: [] } );
            thisStrand = reportStructure.length - 1;
            iStrandLine++;
        }
        else if (iStandardLine < standardLines.length && line == standardLines[iStandardLine].line) {
            reportStructure[thisStrand].standards.push( {
                grade: standardLines[iStandardLine].grade,
                strand: standardLines[iStandardLine].strand,
                standardNum: standardLines[iStandardLine].standardNum,
                standardText: standardLines[iStandardLine].standardText,
                numItems: standardLines[iStandardLine].items,
                numMastered: standardLines[iStandardLine].mastered
            } );
            thisStandard = reportStructure[thisStrand].standards.length - 1;
            iStandardLine++;
            mastered = undefined;
        }
        else if (iOutOfLine < outOfLines.length && line == outOfLines[iOutOfLine])
            iOutOfLine++;  // ignore
        else if (iMasteredLine < masteredLines.length && line == masteredLines[iMasteredLine]) {
            reportStructure[thisStrand].standards[thisStandard].mastered = [];
            mastered = true;
            iMasteredLine++;
        }
        else if (iNotMasteredLine < notMasteredLines.length && line == notMasteredLines[iNotMasteredLine]) {
            reportStructure[thisStrand].standards[thisStandard].notMastered = [];
            mastered = false;
            iNotMasteredLine++;
        }
        else if (iStrandLine >= strandLines.length
                && iStandardLine >= standardLines.length
                && iMasteredLine >= masteredLines.length
                && iNotMasteredLine >= notMasteredLines.length
                && stopExp.test(lines[line])) {
            // finished
            break;
        }
        else {
            if (mastered === undefined)
                issueWarning('Unknown line ' + line + ': ' + lines[line] + ' -- item without mastery indicator?');
            else if (noneExp.test(lines[line]))
                ;  // ignore 'none' item lines
            else if (mastered)
                reportStructure[thisStrand].standards[thisStandard].mastered.push(lines[line].trim());
            else
                reportStructure[thisStrand].standards[thisStandard].notMastered.push(lines[line].trim());
        }
        line++;
    }
}

